分类
联系方式
  1. 新浪微博
  2. E-mail

Dart eval:动态执行 Dart 代码

介绍

dart_eval 是一种基于 Dart AOT 动态执行 Dart 代码的技术,能够实现动态化(CodePush),支持 Flutter。它包含编译器和解释器,均使用 Dart 语言编写,并支持可扩展(如扩展 Flutter 支持)。

dart_eval 由两个 Repo 构成:

  • dart_eval:提供 dart 代码动态执行能力。
  • flutter_eval:基于 dart_eval,扩展 Flutter 代码动态化执行能力。

dart_eval 的主要目标是实现与真实 Dart 代码的互操作性(interoperable)。真实 Dart 代码创建类可以通过一个包装器在 dart_eval 解释器中使用,而在解释器中创建的类,可以通过创建一个接口和桥接类的方式,在解释器之外使用。

dart_eval 的编译器基于 Dart Analyzer 实现,能够实现对 Dart 代码 100% 正确、语法 100% 最新的解析(尽管编译和 evaluation 还没有完全实现)。

目前,dart_eval 实现了相当多的 Dart 规范,但仍然缺少像生成器、集合和扩展方法。此外,许多标准库还没有实现。

使用方式

eval 方法的一个基本使用例子,它是在运行时执行 Dart 代码:

import 'package:dart_eval/dart_eval.dart';

void main() {
  print(eval('2 + 2')); // -> 4
  
  final program = '''
      class Cat {
        Cat(this.name);
        final String name;
        String speak() {
          return name;
        }
      }
      String main() {
        final cat = Cat('Fluffy');
        return cat.speak();
      }
  ''';
  
  print(eval(program, function: 'main')); // -> 'Fluffy'
}

编译到文件

对于大多数使用场景,建议将 Dart 代码预编译为 EVC 字节码,以避免运行时开销。(注:这仍然是运行时代码执行,只是执行了一种更加高效的代码格式)。

这允许你将多个文件编译到一个字节码快:

import 'dart:io';
import 'package:dart_eval/dart_eval.dart';

void main() {
  final compiler = Compiler();
  
  final program = compiler.compile({'my_package': {
    'main.dart': '''
      int main() {
        var count = 0;
        for (var i = 0; i < 1000; i = i + 1) {
          count = count + i;
        }
        return count;
      }
    '''
  }});
  
  final bytecode = program.write();
  
  final file = File('program.evc');
  file.writeAsBytesSync(bytecode);
}

之后你可以加载并执行程序:

import 'dart:io';
import 'package:dart_eval/dart_eval.dart';

void main() {
  final file = File('program.evc');
  final bytecode = file
      .readAsBytesSync()
      .buffer
      .asByteData();
  
  final runtime = Runtime(bytecode);
  runtime.setup();
  print(runtime.executeLib('package:my_package/main.dart', 'main')); // -> 499500
}

使用命令行

dart_eval 命令行允许你将已有的 Dart 工程编译为 EVC 字节码,也包括运行和检查 EVC 字节码的功能。

执行下面命令,让 dart_eval 全局生效:

dart pub global activate dart_eval

编译项目

命令行支持编译标准的 Dart 工程,但是不支持 pubspec.yaml 中声明的依赖。通过下面命令执行编译:

cd my_project
dart_eval compile -o program.evc

这会在当前目录下创建一个名为 program.evc 的 EVC 文件。

编译器也支持用 JSON 编码的桥接绑定文件进行编译。要添加这些,在你的项目根目录下创建一个名为 .dart_eval 的文件夹,添加一个 bindings 子文件夹,并将 JSON 绑定文件放在那里。编译器将自动加载这些绑定,并使它们对你的项目可用。

运行项目

通过下面命令运行生成的 EVC 文件:

dart_eval run program.evc -p package:my_package/main.dart -f main

注意,run 命令不支持绑定。所以任何用绑定编译的文件,都需要在包括必要的运行时绑定的专门运行器中运行。

检查 EVC 文件

使用下面命令 dump EVC 文件的操作码:

dart_eval dump program.evc

比如上面的 Demo,对应 EVC 检查输出如下:

0: PushScope (F3:56, 'Cat.name (get)')
1: PushObjectPropertyImpl (L0[0])
2: Return (L1)
3: PushScope (F3:85, 'Cat.speak()')
4: PushObjectProperty (L0.name)
5: PushReturnValue ()
6: Return (L1)
7: PushScope (F3:30, 'Cat.()')
8: PushNull ()
9: CreateClass (F3:"Cat", super L1, vLen=1))
10: SetObjectPropertyImpl (L2[0] = L0)
11: Return (L2)
12: PushScope (F3:157, 'main()')
13: PushConstant (C0)
14: BoxString (L0)
15: PushArg (L0)
16: Call (@7)
17: PushReturnValue ()
18: PushArg (L1)
19: InvokeDynamic (L1.speak)
20: PushReturnValue ()
21: Return (L2)

返回值

在大多数情况下,dart_eval 将返回 $Value 的一个子类,如 $int$String。这些 "装箱类型 "包含关于它们是什么以及如何修改它们的信息,就像所有的 $Values 一样,你可以用 $value 属性访问它们的底层值。

然而,当处理原始值类型(int,string等)时,你可能会发现 dart_eval 直接返回底层原始值。这是由于内部的性能优化。如果你不喜欢这种不一致,你可以把函数签名的返回类型改为动态,这将迫使 dart_eval 在返回之前总是对值进行装箱。

互操作性(Interop)

Interop 是一个总的术语,我们可以在 Dart 中访问、使用和修改 dart_eval 的数据。实现这种访问是 dart_eval 的首要任务。

互操作性包含 3 个层次:

  • 值互操作性
  • 封装互操作性
  • 桥接互操作性

值互操作性

值互操作是最基本的形式,只要 Eval 环境与一个由真正 Dart 值支持的对象一起工作就会自动发生。(因此,一个 int 和一个字符串是可以进行值互操作的,但是在 Eval 中创建的类是不可以的)。要访问 $Value 的支持对象,请使用其 $value 属性。如果该值是一个集合,如 Map 或 List,你可以使用它的 $reified 属性来解析它所包含的值。

为了支持价值互操作,一个类只需要实现 $Value,或者集成 $Value<T>

封装互操作性

使用封装其可以使 Eval 环境访问在 Eval 之外创建的类上的函数和字段。它比值互操作性更强大,也比桥接互操作更简单,这使得它成为某些用例的最佳选择。要使用包装器互操作,创建一个实现 $Instance 的类。然后,覆盖 $getProperty / $setProperty 来定义你的字段和方法。

桥接互操作性

桥式互操作实现了最多的功能。Eval 不仅可以访问对象的字段,而且还可以进行扩展,允许你在 Eval 中创建子类并在 Eval 之外使用它们。例如,Flightstream 使用桥接互操作来创建自定义的 Flutter 小部件。

然而,它也有点难以使用,而且它不能用来包装在你不控制的代码中创建的现有对象。(为了获得最大的灵活性而牺牲简单性,你可以同时使用桥接和包装器互操作)。由于桥接互操作需要大量的模板代码,在未来我将创建一个解决方案来生成这些模板代码。

桥接互操作还要求类的定义在编译时和运行时都是可用的。(如果你只是使用 eval 方法,你就不必担心这个问题)。

example 目录下有一个以桥接互操作性为特色的例子。

插件

为了配置编译和运行时的互操作,建议创建一个 EvalPlugin,使编译器实例能够被重用。示例:

class MyAppPlugin implements EvalPlugin {
  @override
  String get identifier => 'package:myapp';

  @override
  void configureForCompile(Compiler compiler) {
    compiler.defineBridgeTopLevelFunction(BridgeFunctionDeclaration(
      'package:myapp/functions.dart',
      'loadData',
      BridgeFunctionDef(
          returns: BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.objectType)), params: [])
    ));
    compiler.defineBridgeClass($CoolWidget.$declaration);
  }

  @override
  void configureForRuntime(Runtime runtime) {
    runtime.registerBridgeFunc('package:myapp/functions.dart', 'loadData', 
        (runtime, target, args) => $Object(loadData()));
    runtime.registerBridgeFunc('package:myapp/classes.dart', 'CoolWidget.', $CoolWidget.$new);
  }
}

然后你可以用 Compiler.addPlugin 和 Runtime.addPlugin 使用这个插件。

FAQ

原理是什么?

dart_eval 是一个完全基于 Dart 的字节码编译器和运行时的实现。首先,Dart Analyzer 被用来将代码解析成 AST(抽象语法树)。然后,编译器依次查看每个声明,并递归地编译成线性字节码格式。

对于运行(Evaluation),dart_eval使用Dart的优化动态分发(dispatch)。这意味着每个字节码实际上是一个实现 EvcOp 的类,我们调用其 run() 方法来执行它。字节码可以做一些事情,如在堆栈中推送和弹出值,添加数字,跳转到程序中的其他地方,以及更复杂的Dart特有的操作,如创建一个类。

性能怎么样?

初步测试表明,对于简单的代码,在 AOT 编译的 Dart 中运行的 dart_eval 比标准 AOT Dart 慢12倍左右,大约与 Ruby 这样的语言相当。

对于许多用例来说,这实际上并不重要,例如,在 Flutter 的案例中,应用程序将 99% 的性能开销花在 Flutter 框架本身。

网络资源

dart_eval | Dart Package (pub.dev)